本系列文章內容同步發佈於這裡,若有任何問題或錯誤,都歡迎直接到 GitHub 上發 PR 修正,或是在這裡留言討論。
接續前一章,Route 解讀網址之後,會把工作轉往指定的 Controller 及 Action。在這個小節我們會試著在畫面上跟使用者說聲哈囉,熟悉一下 Route、Controller 以及 View 是怎麼運作的。
Controller 中文可翻譯成「控制器」,顧名思義,就是用來控制流程,例如它可能需要跟 Model 要資料,可能需要跟 View 要 HTML template 來玩填空遊戲,或是可能需要存取外部服務(例如金流串接)等,這大多是 Controller 要做的工作。
在 Rails 的慣例中,Controller 的命名會根據 Route 是使用複數的 resources 還是單數 resource 方法而定。如果在 Route 是使用複數型態,例如:
Rails.application.routes.draw do
resources :posts
resources :users
end
在沒有特別指定 Resources 的 controller 參數的情況下,預設會對到的 Controller 就會是 PostsController 或是 UsersController 這樣的複數型態;反之,如果使用的是單數 resource,對到的就會是單數的 Controller。
在開始之前,讓我們使用 Rails 內建的產生器做一個全新的 Controller:
$ rails g controller pages
Running via Spring preloader in process 16503
create app/controllers/pages_controller.rb
invoke erb
create app/views/pages
invoke test_unit
create test/controllers/pages_controller_test.rb
invoke helper
create app/helpers/pages_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/pages.coffee
invoke scss
create app/assets/stylesheets/pages.scss
上面這行指令會幫你做出一個 PagesController,以及一些其它對應的檔案、目錄。Controller 的內容如下:
class PagesController < ApplicationController
end
這個 Controller 裡什麼內容都沒有,就只有繼承自 ApplicationController 而已。所以如果之後上手的話,也不一定要用產生器來幫你產生 Controller,直接自己手動新增也行。
別忘了,使用者想要看到你網站上的內容,第一步是要問過 Route,所以我們先在 Route 上簡單的加上一條:
Rails.application.routes.draw do
get "hello_world", to: "pages#hello"
resources :posts
resources :users
end
當使用者輸入 /hello_world 網址的時候,會交給 PagesController 的 hello 方法處理。(是的,其實網址跟 Controller 上的 Action 不一定要同名喔)
有了 Route 之後,接下來回到 Controller 把 hello 這個 Action 加上去:
class PagesController < ApplicationController
def hello
render plain: "<h1>你好,世界!</h1>"
end
end
在 hello 方法裡要把文字輸出到瀏覽器上,不是使用 return 也不是使用 puts,而是使用 render 方法,後面的 plain 參數是指要輸出一個一般的文字內容到畫面上。
有些剛開始學 Rails 的新朋友可能會這樣想:
class PagesController < ApplicationController
def hello
render plain: "<h1>你好,世界!</h1>"
puts "---- 你好 ----"
end
end
使用 puts 方法把資料直接輸出在畫面上,看起來很直覺,但這樣不會有效果。事實上並不是 puts 方法不能用,它的確可以把東西印出來,只是不是印在瀏覽器上給你看到,而是印在 Rails 的 log 裡,仔細看一下正在執行 rails server 的那個畫面是不是有這樣的東西:

你就可以看到這樣的畫面:

雖然第 2 步這樣可以把資料輸出在畫面上沒錯,但這樣實在是太辛苦了。在 Controller 裡的 Action,如果沒有特別指定 render 方法的話,它會到 app/views/ 的目錄找「 Controller 名字」目錄裡的 Action 同名檔案。以這個例子來說,它會去找 app/views/pages/hello.html.erb。

如果這個 hello.html.erb 不存在,就自己手動建一個吧。即然輸出的事情交給 View,原來 hello 這個 Action 的 render 方法就可以拿掉:
class PagesController < ApplicationController
def hello
end
end
就這樣空空的,然後編輯 app/views/pages/hello.html.erb
<h1>你好,世界</h1>
<h2>我是設計師也看得懂的檔案喔</h2>
重新整理,應該就會看到跟剛才的差別:

這樣的好處是不用把 HTML 都寫在 Controller 裡(事實上也很少人會這麼做),再來就是要跟設計師合作的時候也比較方便。
接下來我們看看怎麼傳參數給 Controller。當使用者輸入網址這樣的網址:
/hello_world?name=5xruby&price=100&staff=20
畫面的輸出雖然沒變,但後面跟的那串東西會被當做參數傳進一個特別的變數叫做 params。這是 Rails 預先幫我們定義好的,它可以捕捉到這個頁面的資訊。讓我們在剛剛的 hello Action 裡加一些料:
class PagesController < ApplicationController
def hello
render json: params
end
end
使用 render 方法,把 params 這個變數用 JSON 的方式印出來,可以看到這個結果:

Rails 會把剛剛後面那串東西,整理成一個類似 Hash 的東西,例如我只想要 name 參數的話:
class PagesController < ApplicationController
def hello
render plain: params["name"]
end
end

不管是 GET 或是 POST 方式傳過來的參數,都會被收集到這個 params 裡。
大概知道 Route、Controller、View 以及 Params 的使用方法後,接下來我們來做一個可以計算 BMI(Body Mass Index,身體質量指數)的計算機。
先用產生器把 Controller 做出來:
$ rails g controller bmi index 21:53:32
Running via Spring preloader in process 18198
create app/controllers/bmi_controller.rb
route get 'bmi/index'
invoke erb
create app/views/bmi
create app/views/bmi/index.html.erb
invoke test_unit
create test/controllers/bmi_controller_test.rb
invoke helper
create app/helpers/bmi_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/bmi.coffee
invoke scss
create app/assets/stylesheets/bmi.scss
跟前面稍微有點不一樣的是在 Controller 後面多加了 index 這個參數,這樣會自動幫你做幾件事:
Rails.application.routes.draw do
get 'bmi/index'
get "hello_world", to: "pages#hello"
resources :posts
resources :users
end
多加了 get 'bmi/index 條路徑。但我不是很喜歡這樣的路徑,所以請把它改成:
Rails.application.routes.draw do
get "bmi", to: "bmi#index"
get "hello_world", to: "pages#hello"
resources :posts
resources :users
end
這時候輸入路徑 /bmi 應該可以看到這個畫面:

index Action:class BmiController < ApplicationController
def index
end
end
app/views/bmi/index.html.erb 檔案內容如下:
<h1>Bmi#index</h1>
<p>Find me in app/views/bmi/index.html.erb</p>
編輯 app/views/bmi/index.html.erb 如下:
<h1>BMI 計算機</h1>
<%= form_tag '/bmi/result' do %>
身高:<%= text_field_tag 'body_height' %> 公分<br />
體重:<%= text_field_tag 'body_weight' %> 公斤<br />
<%= submit_tag "開始計算" %>
<% end %>
這裡有幾個需要稍做說明的地方:
form_tag 會被轉換成 HTML 的 <form> 標籤text_field_tag 會被轉換成 HTML 的 <input type="text" /> 標籤。submit_tag 會被轉換成 HTML 的 <input type="submit" /> 標籤。以上這些方法都統稱為 View Helper,更多相關的使用方法參考這裡。詳細轉換內容可直接檢視頁面原始碼。這時候的畫面會長得像這樣:

這時候當我們按下送出的時候會得到 Routing Error 的錯誤訊息,那是因為我們還沒有這個路徑,所以現在來補做一下。在 Route 裡加上一行:
Rails.application.routes.draw do
get "bmi", to: "bmi#index"
post "bmi/result", to: "bmi#result"
get "hello_world", to: "pages#hello"
resources :posts
resources :users
end
這樣可讓 Route 可以接到 POST 並轉往 BmiController 的 result Action。
Route 有了,接下來就是把 result Action 的內容補上去:
class BmiController < ApplicationController
def index
end
def result
height = params[:body_height].to_f / 100 # 把單位換算成公尺
weight = params[:body_weight].to_f
# BMI 計算公式: BMI = 體重(單位:公斤) / 身高平方(單位:公尺).
@bmi = (weight / (height * height)).round(2)
end
end
BMI 的計算公式還滿單純的,不過要注意的是:
params 取得的資料預設型態是字串,所以需要使用 .to_i 或 .to_f 轉換成數字。這裡因為需要使用除法計算到小數點以下所以使用 .to_f 方法進行轉換。@bmi,以便讓 View 可以取用。最後一步,把結果印出來。編輯檔案 app/views/bmi/result.html.erb (如果檔案不存在請直接手動建立):
<h1>您的 BMI 值為:<%= @bmi %></h1>
搞定! 試玩一下,我輸入身高 178 公分、體重 80 公斤:

按下送出即可得到計算結果:

雖然這個計算機的功能相當陽春,而且也很多地方需要改善,例如防呆機制,或是根據計算結果嘲諷一下 BMI 值過高的胖子。但如果你能理解這個例子裡 Route、Controller、View 之間的基本運作原理,當下回遇到更複雜的應用程式開發相信也是可以迎刃而解。
以上實作完整程式碼可在這裡取得。
本系列文章內容同步發佈於這裡,若有任何問題或錯誤,都歡迎直接到 GitHub 上發 PR 修正,或是在這裡留言討論。